package net.avcompris.binding;

import static com.avcompris.util.ExceptionUtils.nonNullArgument;

import java.lang.reflect.Method;

import net.avcompris.binding.annotation.Functions;
import net.avcompris.binding.annotation.Namespaces;
import net.avcompris.binding.annotation.Nodes;
import net.avcompris.binding.annotation.XPath;

import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder;

import com.avcompris.common.annotation.Nullable;
import com.avcompris.lang.NotImplementedException;
import com.google.common.collect.ImmutableSet;

/**
 * holder for configuration parameters that also may be passe via the
 * {@link XPath}, {@link Namespaces} and {@link Nodes} annotations.
 * <p>
 * One cannot instantiate this class directly: You must use a
 * {@link #Builder}.
 * 
 * @author David Andrianavalontsalama
 */
public final class BindConfiguration {

	/**
	 * constructor is private. Please use {@link #Builder}.
	 */
	private BindConfiguration(
			final String xpathExpression,
			@Nullable final String xpathFunction,
			@Nullable final String xpathFailFunction,
			@Nullable final String[] namespaces,
			@Nullable final Class<?> collectionComponentType,
			@Nullable final String mapKeysXPath,
			@Nullable final String mapKeysXPathFunction,
			@Nullable final Class<?> mapKeysType,
			@Nullable final String mapValuesXPath,
			@Nullable final String mapValuesXPathFunction,
			@Nullable final Class<?> mapValuesType,
			final boolean nodesElementsEverywhere,
			final boolean nodesEmptyAttributes,
			final boolean nodesElementNames) {

		this.xpathExpression = nonNullArgument(xpathExpression,
				"xpathExpression");
		this.xpathFunction = xpathFunction;
		this.xpathFailFunction = xpathFailFunction;
		this.namespaces = namespaces;
		this.nodesElementsEverywhere = nodesElementsEverywhere;
		this.nodesEmptyAttributes = nodesEmptyAttributes;
		this.nodesElementNames=nodesElementNames;
	}

	public String getXPathExpression() {

		return xpathExpression;
	}

	@Nullable
	public String getXPathFunction() {

		return xpathFunction;
	}

	@Nullable
	public String getXPathFailFunction() {

		return xpathFailFunction;
	}

	public boolean hasXPath() {

		return hasXPathExpression();
	}

	public boolean hasXPathExpression() {

		return xpathExpression != null;
	}

	@Nullable
	public String[] getNamespaces() {

		return namespaces;
	}

	public boolean isNodesElementsEverywhere() {

		return nodesElementsEverywhere;
	}

	public boolean isNodesEmptyAttributes() {

		return nodesEmptyAttributes;
	}

	public boolean isNodesElementNames() {

		return nodesElementNames;
	}

	private final String xpathExpression;

	@Nullable
	private final String xpathFunction;

	@Nullable
	private final String xpathFailFunction;

	@Nullable
	private final String[] namespaces;

	private final boolean nodesElementsEverywhere;
	private final boolean nodesEmptyAttributes;
	private final boolean nodesElementNames;

	@Override
	public int hashCode() {

		return HashCodeBuilder.reflectionHashCode(this);
	}

	@Override
	public boolean equals(@Nullable final Object o) {

		return EqualsBuilder.reflectionEquals(this, o);
	}

	/**
	 * extract the configuration information from an interface annotated
	 * with {@link XPath} and/or {@link Namespaces} and/or {@link Nodes}.
	 */
	public static ClassBinding readClassBinding(final Class<?> clazz) {

		nonNullArgument(clazz, "class");

		//final XPath xpath = clazz.getAnnotation(XPath.class);
		//final Namespaces namespaces = clazz.getAnnotation(Namespaces.class);
		//final Nodes nodes = clazz.getAnnotation(Nodes.class);

		throw new NotImplementedException();
	}

	/**
	 * extract the configuration information from a method annotated
	 * with {@link XPath} and/or {@link Namespaces} and/or {@link Nodes}.
	 */
	public static MethodBinding readMethodBinding(final Method method) {

		nonNullArgument(method, "method");

		throw new NotImplementedException();
	}

	/**
	 * extract the configuration information for all methods annotated
	 * with {@link XPath} and/or {@link Namespaces} and/or {@link Nodes}
	 * within a given class.
	 */
	public static ImmutableSet<MethodBinding> readMethodBindings(
			final Class<?> clazz) {

		nonNullArgument(clazz, "class");

		throw new NotImplementedException();
	}

	/**
	 * use a builder to create a {@link BindConfiguration} object.
	 */
	public static Builder newBuilder() {

		return newBuilder((BindConfiguration) null);
	}

	/**
	 * use a builder to create a {@link BindConfiguration} object.
	 */
	public static Builder newBuilder(
			@Nullable final BindConfiguration configuration) {

		return new Builder(configuration);
	}

	/**
	 * use a builder to create a {@link BindConfiguration} object.
	 */
	public static Builder newBuilder(final String xpathExpression) {

		return newBuilder().setXPathExpression(xpathExpression);
	}

	/**
	 * use a builder to create a {@link BindConfiguration} object.
	 */
	public static final class Builder {

		protected Builder() {

			this(null);
		}

		protected Builder(@Nullable final BindConfiguration configuration) {

			if (configuration != null) {

				xpathExpression = configuration.xpathExpression;
				xpathFunction = configuration.xpathFunction;
				namespaces = configuration.namespaces;
				nodesElementsEverywhere = configuration.nodesElementsEverywhere;
				nodesEmptyAttributes = configuration.nodesEmptyAttributes;
				nodesElementNames = configuration.nodesElementNames;
			}
		}

		public synchronized Builder setXPath(@Nullable final XPath xpath) {

			if (xpath == null) {

				return setXPathExpression(DEFAULT_XPATH_EXPRESSION)
						.setXPathFunction(DEFAULT_XPATH_FUNCTION);

			} else {

				return setXPathExpression(xpath.value()).setXPathFunction(
						xpath.function());
			}
		}

		public synchronized Builder setXPathExpression(
				final String xpathExpression) {

			nonNullArgument(xpathExpression, "xpathExpression");

			this.xpathExpression = xpathExpression;

			return this;
		}

		public synchronized Builder setXPathFunction(
				@Nullable final String xpathFunction) {

			this.xpathFunction = xpathFunction;

			return this;
		}

		public synchronized Builder setNamespaces(
				@Nullable final Namespaces namespaces) {

			if (namespaces == null) {

				return setNamespaces(DEFAULT_NAMESPACES);

			} else {

				return setNamespaces(namespaces.value());
			}
		}

		public synchronized Builder setNamespaces(
				@Nullable final String[] namespaces) {

			if (namespaces == null) {

				this.namespaces = DEFAULT_NAMESPACES;

			} else {

				this.namespaces = namespaces;
			}

			return this;
		}

		public synchronized Builder setNodes(@Nullable final Nodes nodes) {

			if (nodes == null) {

				return setNodesElementsEverywhere(
						DEFAULT_NODES_ELEMENTS_EVERYWHERE)
						.setNodesEmptyAttributes(DEFAULT_NODES_EMPTY_ATTRIBUTES)
						.setNodesElementNames(DEFAULT_NODES_ELEMENT_NAMES);

			} else {

				return setNodesElementsEverywhere(nodes.elementsEverywhere())
						.setNodesEmptyAttributes(nodes.emptyAttributes())
						.setNodesElementNames(nodes.elementNames());
			}
		}

		public synchronized Builder setNodesElementsEverywhere(
				final boolean nodesElementsEverywhere) {

			this.nodesElementsEverywhere = nodesElementsEverywhere;

			return this;
		}

		public synchronized Builder setNodesEmptyAttributes(
				final boolean nodesEmptyAttributes) {

			this.nodesEmptyAttributes = nodesEmptyAttributes;

			return this;
		}

		public synchronized Builder setNodesElementNames(
				final boolean nodesElementNames) {

			this.nodesElementNames = nodesElementNames;

			return this;
		}

		public synchronized BindConfiguration build() {

			return new BindConfiguration( // 
					xpathExpression, xpathFunction, xpathFailFunction, //
					namespaces, //
					collectionComponentType, //
					mapKeysXPath, mapKeysXPathFunction, mapKeysType, //
					mapValuesXPath, mapValuesXPathFunction, mapValuesType, //
					nodesElementsEverywhere, nodesEmptyAttributes, nodesElementNames);
		}

		private String xpathExpression = DEFAULT_XPATH_EXPRESSION;
		private String xpathFunction = DEFAULT_XPATH_FUNCTION;
		private String xpathFailFunction = DEFAULT_XPATH_FUNCTION;

		private Class<?> collectionComponentType = Object.class;
		private String mapKeysXPath = DEFAULT_XPATH_EXPRESSION;
		private String mapKeysXPathFunction = DEFAULT_XPATH_FUNCTION;
		private Class<?> mapKeysType = Object.class;
		private String mapValuesXPath = DEFAULT_XPATH_EXPRESSION;
		private String mapValuesXPathFunction = DEFAULT_XPATH_FUNCTION;
		private Class<?> mapValuesType = String.class;

		private String[] namespaces = DEFAULT_NAMESPACES;

		private boolean nodesElementsEverywhere = DEFAULT_NODES_ELEMENTS_EVERYWHERE;
		private boolean nodesEmptyAttributes = DEFAULT_NODES_EMPTY_ATTRIBUTES;
		private boolean nodesElementNames = DEFAULT_NODES_ELEMENT_NAMES;
	}

	/**
	 * default value is <tt>"/&#42;"</tt>
	 */
	public static final String DEFAULT_XPATH_EXPRESSION = "/*";

	/**
	 * default value is <tt>null</tt>
	 */
	public static final String DEFAULT_XPATH_FUNCTION = null;

	/**
	 * default value is <tt>null</tt>
	 */
	public static final String[] DEFAULT_NAMESPACES = null;

	/**
	 * default value is <tt>false</tt>
	 */
	public static final boolean DEFAULT_NODES_ELEMENTS_EVERYWHERE = false;

	/**
	 * default value is <tt>false</tt>
	 */
	public static final boolean DEFAULT_NODES_EMPTY_ATTRIBUTES = false;

	/**
	 * default value is <tt>true</tt>
	 */
	public static final boolean DEFAULT_NODES_ELEMENT_NAMES = true;
	
	/**
	 * add the declaration of a XPath function.
	 * 
	 * @param clazz the function class, as it appears in the
	 * {@link Functions} annotation.
	 * @param instance the instance to use.
	 */
	public <T extends Functions> void addFunctions(final Class<T> clazz,
			final T instance) {

		nonNullArgument(clazz, "class");
		nonNullArgument(instance, "instance");

		throw new NotImplementedException();
	}
}
